/**
 * Helpers
 */
const escapeTest = /[&<>"']/;
const escapeReplace = new RegExp(escapeTest.source, "g");
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, "g");
const escapeReplacements = {
	"&": "&amp;",
	"<": "&lt;",
	">": "&gt;",
	'"': "&quot;",
	"'": "&#39;"
};
const getEscapeReplacement = ch => escapeReplacements[ch];
export function escape(html, encode) {
	if (encode) {
		if (escapeTest.test(html)) {
			return html.replace(escapeReplace, getEscapeReplacement);
		}
	} else {
		if (escapeTestNoEncode.test(html)) {
			return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
		}
	}

	return html;
}

const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;

/**
 * @param {string} html
 */
export function unescape(html) {
	// explicitly match decimal, hex, and named HTML entities
	return html.replace(unescapeTest, (_, n) => {
		n = n.toLowerCase();
		if (n === "colon") return ":";
		if (n.charAt(0) === "#") {
			return n.charAt(1) === "x"
				? String.fromCharCode(parseInt(n.substring(2), 16))
				: String.fromCharCode(+n.substring(1));
		}
		return "";
	});
}

const caret = /(^|[^\[])\^/g;

/**
 * @param {string | RegExp} regex
 * @param {string} opt
 */
export function edit(regex, opt) {
	regex = typeof regex === "string" ? regex : regex.source;
	opt = opt || "";
	const obj = {
		replace: (name, val) => {
			val = val.source || val;
			val = val.replace(caret, "$1");
			regex = regex.replace(name, val);
			return obj;
		},
		getRegex: () => {
			return new RegExp(regex, opt);
		}
	};
	return obj;
}

const nonWordAndColonTest = /[^\w:]/g;
const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;

/**
 * @param {boolean} sanitize
 * @param {string} base
 * @param {string} href
 */
export function cleanUrl(sanitize, base, href) {
	if (sanitize) {
		let prot;
		try {
			prot = decodeURIComponent(unescape(href))
				.replace(nonWordAndColonTest, "")
				.toLowerCase();
		} catch (e) {
			return null;
		}
		if (
			prot.indexOf("javascript:") === 0 ||
			prot.indexOf("vbscript:") === 0 ||
			prot.indexOf("data:") === 0
		) {
			return null;
		}
	}
	if (base && !originIndependentUrl.test(href)) {
		href = resolveUrl(base, href);
	}
	try {
		href = encodeURI(href).replace(/%25/g, "%");
	} catch (e) {
		return null;
	}
	return href;
}

const baseUrls = {};
const justDomain = /^[^:]+:\/*[^/]*$/;
const protocol = /^([^:]+:)[\s\S]*$/;
const domain = /^([^:]+:\/*[^/]*)[\s\S]*$/;

/**
 * @param {string} base
 * @param {string} href
 */
export function resolveUrl(base, href) {
	if (!baseUrls[" " + base]) {
		// we can ignore everything in base after the last slash of its path component,
		// but we might need to add _that_
		// https://tools.ietf.org/html/rfc3986#section-3
		if (justDomain.test(base)) {
			baseUrls[" " + base] = base + "/";
		} else {
			baseUrls[" " + base] = rtrim(base, "/", true);
		}
	}
	base = baseUrls[" " + base];
	const relativeBase = base.indexOf(":") === -1;

	if (href.substring(0, 2) === "//") {
		if (relativeBase) {
			return href;
		}
		return base.replace(protocol, "$1") + href;
	} else if (href.charAt(0) === "/") {
		if (relativeBase) {
			return href;
		}
		return base.replace(domain, "$1") + href;
	} else {
		return base + href;
	}
}

export const noopTest = { exec: function noopTest() {} };

export function splitCells(tableRow, count) {
	// ensure that every cell-delimiting pipe has a space
	// before it to distinguish it from an escaped pipe
	const row = tableRow.replace(/\|/g, (match, offset, str) => {
			let escaped = false,
				curr = offset;
			while (--curr >= 0 && str[curr] === "\\") escaped = !escaped;
			if (escaped) {
				// odd number of slashes means | is escaped
				// so we leave it alone
				return "|";
			} else {
				// add space before unescaped |
				return " |";
			}
		}),
		cells = row.split(/ \|/);
	let i = 0;

	// First/last cell in a row cannot be empty if it has no leading/trailing pipe
	if (!cells[0].trim()) {
		cells.shift();
	}
	if (cells.length > 0 && !cells[cells.length - 1].trim()) {
		cells.pop();
	}

	if (cells.length > count) {
		cells.splice(count);
	} else {
		while (cells.length < count) cells.push("");
	}

	for (; i < cells.length; i++) {
		// leading or trailing whitespace is ignored per the gfm spec
		cells[i] = cells[i].trim().replace(/\\\|/g, "|");
	}
	return cells;
}

/**
 * Remove trailing 'c's. Equivalent to str.replace(/c*$/, '').
 * /c*$/ is vulnerable to REDOS.
 *
 * @param {string} str
 * @param {string} c
 * @param {boolean} invert Remove suffix of non-c chars instead. Default falsey.
 */
export function rtrim(str, c, invert) {
	const l = str.length;
	if (l === 0) {
		return "";
	}

	// Length of suffix matching the invert condition.
	let suffLen = 0;

	// Step left until we fail to match the invert condition.
	while (suffLen < l) {
		const currChar = str.charAt(l - suffLen - 1);
		if (currChar === c && !invert) {
			suffLen++;
		} else if (currChar !== c && invert) {
			suffLen++;
		} else {
			break;
		}
	}

	return str.slice(0, l - suffLen);
}

export function findClosingBracket(str, b) {
	if (str.indexOf(b[1]) === -1) {
		return -1;
	}
	const l = str.length;
	let level = 0,
		i = 0;
	for (; i < l; i++) {
		if (str[i] === "\\") {
			i++;
		} else if (str[i] === b[0]) {
			level++;
		} else if (str[i] === b[1]) {
			level--;
			if (level < 0) {
				return i;
			}
		}
	}
	return -1;
}

export function checkSanitizeDeprecation(opt) {
	if (opt && opt.sanitize && !opt.silent) {
		console.warn(
			"marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options"
		);
	}
}

// copied from https://stackoverflow.com/a/5450113/806777
/**
 * @param {string} pattern
 * @param {number} count
 */
export function repeatString(pattern, count) {
	if (count < 1) {
		return "";
	}
	let result = "";
	while (count > 1) {
		if (count & 1) {
			result += pattern;
		}
		count >>= 1;
		pattern += pattern;
	}
	return result + pattern;
}
